這幾天的內容中,我們已經學到了幾個重點:
extends
限制泛型keyof
的使用現在讓我們結合這幾天的內容來試著寫個簡單的函式,這個函式名稱是 getObjValue
,功能很簡單,它可以接受兩個參數,第一個參數是物件,第二個參數是該物件中的 key,回傳的內容就是物件中對應到該 key 的 value。最終寫起來會像這樣:
使用方式會像這樣:
const user = {
firstName: 'PJ',
lastName: 'Chen',
age: 35,
isAdmin: true,
};
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
const isAdmin = getObjValue(user, 'isAdmin'); // true
const manufacturer = getObjValue(product, 'manufacturer'); // 'Apple'
同樣的,如果你原本就已經看得懂上面這個函式的寫法,歡迎直接左轉去看我同事 Kyle 「今晚,我想來點 Web 前端效能優化大補帖!」的精彩文章!。
首先,讓我們先不管 TypeScript,用原有 JavaScript 的知識寫出這個函式,寫起來會像這樣:
const getObjValue = (obj, key) => obj[key];
但這個函式在沒有型別保護的情況下,很有可能會呼叫到了根本不存在該 obj 中的 key,進而取不到值。例如,我們以為 user 物件中有 title
這個屬性,但實際上卻沒有:
// 在沒有型別保護的情況下,很有可能會呼叫到了根本不存在該 obj 中的 key,進而取不到值
const title = getObjValue(user, 'title'); // undefined
如果我們又直接拿這個 title
去做其他的操作,就有可能會發生錯誤。
現在請讀者試著把它改成 TypeScript 的寫法。
你可以把物件 user
和函式 getObjValue
貼到 TypeScript Playground 中練習看看。
預設的情況下,如果我們沒有定義參數的型別,它的型別會是 any
:
但這並不是我們想要的情況,因為 any
代表 TypeScript 完全無法掌握這個函式的型別,也同樣無法避免去取到該物件中不存在的屬性。
我們可以用剛剛定義好的 user
物件來想想看要怎麼定義這個函式的型別。首先因為函式的參數 obj
和 key
都需要被明確的給予型別,所以可以:
User
型別obj
可以接受的型別是 User
key
需要是 obj 中帶有的屬性 key,寫起來會像這樣:
// STEP 1:定義 `User` 的型別
type User = {
firstName: string;
lastName: string;
age: number;
isAdmin: boolean;
};
// STEP 2:使用 `User` 的型別來定義 `obj` 和 `key` 的型別
const getObjValue = (
obj: User,
key: 'firstName' | 'lastName' | 'age' | 'isAdmin'
) => obj[key];
你會注意到,在 key
的型別中,我們列出了所有 User 這個物件型別可能有的屬性名稱('firstName' | 'lastName' | 'age' | 'isAdmin'
),稍微思考一下前幾週所學的內容,應該會發現可以用 keyof
來取代這樣的寫法,否則未來如果 user
的物件有添加新的屬性時,就還需要去修改 key
的型別定義,非常不方便,也不符合 single source of truth 的原則。
因此可以把函式改成這樣:
是不是精簡了不少?
這時候就可以確保使用 getObjValue
的開發者不會發生想要在 user
物件中去取得 title
屬性的情況,因為 TypeScript 會知道 title
這個屬性並不存在 User
中,它會直接報錯:
這樣做雖然可以避免開發者誤用不存在 User 型別中的屬性,但因為我們指定了 obj
的型別是 User
,進而導致這個函式變成只能針對 User
型別才能使用,如果 obj
不是 User 的話,就完全沒辦法再使用 getObjValue
。
例如,現在想要取出的是 product 物件中 name
屬性的 value,但因為 product
物件不滿足先前定義的 User
型別,所以 TypeScript 會報錯:
const getObjValue = (obj: User, key: keyof User) => obj[key];
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
// product 並不滿足 User 的型別
getObjValue(product, 'name'); // ❌ TypeScript compile error
我們不會想要每當有不同的物件時,就寫一個新的、但功能完全一樣的函式,這樣 getObjValue
就太不好用了:
// 如果不使用泛型...
const getObjValueOfUser = (obj: User, key: keyof User) => obj[key];
const getObjValueOfProduct = (obj: Product, key: keyof Product) => obj[key];
這時候你是否有回憶起前幾天提到「泛型」這個好用的東東,讓我們試著把上面共同的部分抽出來,變成一個泛型的變數:
可以看到,User
和 Product
就是可以被抽出來變成泛型變數的部分,變成這樣:
function getObjValue<T>(obj: T, key: keyof T) {
return obj[key];
}
如此,這個函式就可以同時帶入任何的物件而不需要重複定義函式,如果使用者用了物件中不存在的屬性時 TypeScript 一樣會提出警告:
const user = {
firstName: 'PJ',
lastName: 'Chen',
age: 35,
isAdmin: true,
};
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
function getObjValue<T>(obj: T, key: keyof T) {
return obj[key];
}
const isAdmin = getObjValue(user, 'isAdmin'); // true
const manufacturer = getObjValue(product, 'manufacturer'); // 'Apple'
如同在第二天針對泛型所提到的,如果沒有在
<>
中指定泛型參數的型別,TypeScript 會自動根據帶入函式的參數來推導泛型的型別(type argument inference),這就是為什麼這裡可以不用寫成getObjValue<User>(...)
這種明確告知泛型型別的方式。
上面 getObjValue
一般來說使用上已經沒有什麼問題了,但如果我們希望把 key
的型別也變成一個泛型的參數,讓使用這個函式的開發者可以自己決定要帶入的 key 型別是什麼時(例如,只能取出該物件中的部分屬性),可以怎麼做呢?
這時候第一步就是把 key
的型別,也變成一個泛型的參數,這裡稱作 U
,讓使用者有自行決定 key 的型別(U
)的機會:
function getObjValue<T, U>(obj: T, key: U) {
return obj[key];
}
但這時候 TypeScript 會報錯:
這個錯誤的意思是說,TypeScript 沒辦法確認 U 一定是 T 的 index,換成比較好理解的方式就是,因為 U
太泛了,沒辦法保證物件 T 中有 U 這個 key 存在。為了要解決這個問題,可以使用在 Day03 學到的泛型限制,透過 extends
來確保 U
一定是物件型別 T 裡存在的 key:
就像這樣,使用了 U extends keyof T
的方式,來確保泛型 U 一定滿足物件型別 T 的 key。
如果我們把滑鼠移到 getObjValue
這個函式時,留意一下它寫的這個函式會回傳的型別,你會發現它寫的是 T[U]
,而這不就是我們昨天提到的 indexed access types 嗎?T
是物件型別,U
是物件的 key,T[U]
就是該屬性值的型別:
最後,使用者如有需要可以自行指定泛型的型別,並限制 getObjValue
能夠取用的 key 的型別:
type Product = {
name: string;
price: number;
manufacturer: string;
madeIn: string;
};
// 限制這裡的 getObjValue 只能取用物件中的 'manufacturer' | 'price' 這兩個屬性
const age = getObjValue<Product, 'manufacturer' | 'price'>(
product,
'manufacturer'
);
如果在定義 key 的型別時,不小心寫了原本就不存在物件中的屬性時,TypeScript 一樣會提早告知錯誤。例如這裡,試著在定義 key 的型別時,取用了不存在 Product 型別中的屬性 key age
,TS 就會跳出錯誤:
這個簡單的 getObjValue
函式,就用了多個前幾天提到的知識點,如果有對那個部分感到不熟悉,都可以翻閱前幾天的內容對照著看。
extends
來限制泛型,但如果能在不限制的形況下就完成想要的功能,就不要給予多餘的限制https://tsplay.dev/mqvrQW @ TypeScript Playground